/**
 * Copyright (c) 2010 Simple Graph Tools contributors. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 *
 * $Id$
 */
package org.sgt.datamodel.util;

import java.util.Map.Entry;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EFactory;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EParameter;
import org.sgt.datamodel.Action;
import org.sgt.datamodel.DataStore;
import org.sgt.datamodel.DatamodelPackage;
import org.sgt.datamodel.MultiPrimitive;
import org.sgt.datamodel.MultiReference;
import org.sgt.datamodel.Primitive;
import org.sgt.datamodel.PrimitiveValue;
import org.sgt.datamodel.Reference;
import org.sgt.datamodel.ReferenceValue;
import org.sgt.datamodel.Value;

/**
 * A default executor for create actions.
 * @author BUTTIGHOFFER-A
 *
 */
public class CreateActionExecutor {

	/**
	 * The maximum number of entity or primitive
	 * types created for the 'unlimited' multiplicity : 10.  
	 */
	public static final int MAX_LIM = 10;

	public static final void executeCreateAction(Action action, DataStore dataStore){
		if (action.getParameters() != null
				&& action.getOutputs().size() == 1) {
			Entry<String, Value> entry = action.getOutputs().get(0);
			String key = entry.getKey();
			Value value = entry.getValue();
			EParameter paramSpec = action.getSpecification().getEParameters().get(0);
			EClassifier specClassifier = paramSpec.getEType();
			
			EClass specEClass = null;
			EDataType specEDataType = null;
			EObject specEObject = null;
			boolean defaultAdded = false;
			
			if (specClassifier instanceof EClass){
				specEClass = (EClass) specClassifier;
				// Create a new EObject with required type.
				specEObject = createEObject(specEClass);
			}
			else if (specClassifier instanceof EDataType){
				specEDataType = (EDataType) specClassifier;
			}

			if (value instanceof ReferenceValue) {
				if (specEDataType != null){
					//Reference to a primitive
					handleReferenceValue(key, value, specEDataType, action, dataStore);
				} else {
					//Reference to an EClass
					handleReferenceValue(key, value, specEClass, specEObject, defaultAdded, paramSpec, action, dataStore);
				}
			} else {
				// We must have a PrimitiveValue
				PrimitiveValue primVal = (PrimitiveValue) value;
				EDataType type = primVal.getType();

				if (type == specClassifier) {
					// Create a new Object inside the dataStore with required
					// type.
					if (primVal instanceof MultiPrimitive) {
						EList<String> stringValues = new BasicEList<String>(
								((MultiPrimitive) primVal).getStringValues());
						for (String stringValue : stringValues) {
							Object object = createObject(type, stringValue);
							((MultiPrimitive) primVal).getValues().add(object);
						}
					}
					if (primVal instanceof Primitive) {
						Object object = createObject(type,
								((Primitive) primVal).getStringValue());
						((Primitive) primVal).setValue(object);
					}

					// Add the PrimitiveValue to DataStore.
					dataStore.getEObjects().add(primVal);
					// update index map if we can...
					if (key != null) {
						EList<EObject> list = new BasicEList<EObject>();
						list.add(primVal);
						dataStore.getKnownEObjects().put(key, list);
					}
				}
				else {
					// Our action is not valid...
					ExecutionException execEx = new ExecutionException(
							"Invalid Action :" + action);
					throw execEx;
				}
			}
			
			if (defaultAdded){
				// The default EObject has been used in a reference
				// add it to the data store (no dangling href)
				dataStore.getEObjects().add(specEObject);
				// update index map if we can...
				if (key != null ){
					EList<EObject> list = new BasicEList<EObject>();
					list.add(specEObject);
					dataStore.getKnownEObjects().put(key, list);
				}
			}

		} else {
			// We do not handle this yet...
			ExecutionException execEx = new ExecutionException(
					"Not yet implemented");
			throw execEx;
		}
		return;
	}

	private static void handleReferenceValue(String key, Value value,
			EClass specEClass, EObject specEObject, boolean defaultAdded,
			EParameter paramSpec, Action action, DataStore dataStore) {
		ReferenceValue refVal = (ReferenceValue) value;
		EClass type = refVal.getType();

		if (type == specEClass
			|| specEClass.isSuperTypeOf(type) ){
			int numObjects = paramSpec.getUpperBound();
			if (numObjects == -1){
				numObjects = MAX_LIM;
			}
			createDirectEObject(refVal, specEObject, dataStore, numObjects);
			
		} else if (DatamodelPackage.eINSTANCE.getValue().isSuperTypeOf(type)){
			EList<Value> refValues = buildReferencedValuesLis(refVal, action);

			for (Value refValue : refValues){

				if (refValue instanceof ReferenceValue){
					// A reference or a Multi reference is used to point to our values...

					ReferenceValue reference = (ReferenceValue) refValue;
																			
					if (reference instanceof Reference) {
						Reference theRef = (Reference) reference;
						EClass refType = theRef.getType();
						if (refType == null || refType == specEClass) {
							theRef.setType(specEClass);
							theRef.setReference(specEObject);
							defaultAdded = true;
						} else if (specEClass.isSuperTypeOf(refType)){
							EObject eObject = createEObject(refType);
							dataStore.getEObjects().add(eObject);
							// update index map if we can...
							if (key != null ){
								EList<EObject> list = new BasicEList<EObject>();
								list.add(eObject);
								dataStore.getKnownEObjects().put(key, list);
							}
							theRef.setReference(eObject);
						}
						else {
							// Our action is not valid...
							ExecutionException execEx = new ExecutionException(
									"Invalid Action :" + action);
							throw execEx;					
						}
					}

					if (reference instanceof MultiReference) {
						MultiReference multiRef = (MultiReference) reference;
						for (EObject eObj : multiRef.getReferences()) {
							if (eObj instanceof Reference) {
								Reference theRef = (Reference) eObj;
								if (theRef.getType() == specEClass
										|| specEClass.isSuperTypeOf(theRef
												.getType())) {
									theRef.setReference(specEObject);
									defaultAdded = true;
								}
							}
						}
					}
				} else {
					// Our action is not valid...
					ExecutionException execEx = new ExecutionException(
							"Invalid Action :" + action);
					throw execEx;					
				}
			}
		} else {
			// Our action is not valid...
			ExecutionException execEx = new ExecutionException(
					"Invalid Action :" + action);
			throw execEx;					
		}
	}

	private static void handleReferenceValue(String key, Value value,
			EDataType specEDataType, Action action,
			DataStore dataStore) {

		ReferenceValue refVal = (ReferenceValue) value;
		EClass type = refVal.getType();

		if (DatamodelPackage.eINSTANCE.getValue().isSuperTypeOf(type)){
			EList<Value> refValues = buildReferencedValuesLis(refVal, action);

			for (Value refValue : refValues){

				if (refValue instanceof PrimitiveValue){
					// We need to create a PrimitiveValue

					PrimitiveValue prim = (PrimitiveValue) refValue;
					prim.setType(specEDataType);
					createDirectSingleObject(prim);
				
				}
				else {
					// Our action is not valid...
					ExecutionException execEx = new ExecutionException(
							"Invalid Action :" + action);
					throw execEx;					
				}
			}
		} else {
			// Our action is not valid...
			ExecutionException execEx = new ExecutionException(
					"Invalid Action :" + action);
			throw execEx;					
		}
	}


	private static final EObject createEObject(EClass eClass) throws ExecutionException{
		if (eClass == null) return null;
		
		EPackage ePack = eClass.getEPackage();
		String nsURI = ePack.getNsURI();
		EFactory factory = EPackage.Registry.INSTANCE.getEFactory(nsURI);
		if (factory == null && ePack != null){
			// Use dynamic factory...
			factory = ePack.getEFactoryInstance();
		}
		EObject eObject = null;
	
		if (factory != null){
			eObject = factory.create(eClass);
		} else {
			ExecutionException execEx = new ExecutionException("Could not create Type : "
					+ nsURI + "#" + eClass.getName());
			throw execEx;
		}
	
		return eObject;
	}

	private static final Object createObject(EDataType eDataType, String stringValue) throws ExecutionException{
		if (eDataType == null) return null;
		
		EPackage ePack = eDataType.getEPackage();
		String nsURI = ePack.getNsURI();
		EFactory factory = EPackage.Registry.INSTANCE.getEFactory(nsURI);
		if (factory == null && ePack != null){
			// Use dynamic factory...
			factory = ePack.getEFactoryInstance();
		}
		Object object = null;
	
		if (factory != null){
			object = factory.createFromString(eDataType, stringValue);
		} else {
			ExecutionException execEx = new ExecutionException("Could not create Type : "
					+ nsURI + "#" + eDataType.getName());
			throw execEx;
		}
	
		return object;
	}

	private static final void createValue(MultiPrimitive primitive){
		if (primitive == null) return;
		
		EDataType eDataType = primitive.getType();
		if (eDataType == null) return;
		
		EPackage ePack = eDataType.getEPackage();
		String nsURI = ePack.getNsURI();
		EFactory factory = EPackage.Registry.INSTANCE.getEFactory(nsURI);
	
		EList<String> stringValues = primitive.getStringValues();
		for (int i = 0 ; i < stringValues.size(); i++){
			String stringValue = stringValues.get(i);
			if (stringValue == null){
				Object obj = eDataType.getDefaultValue();
				if (obj != null){
					if (factory != null){
						primitive.getStringValues().add(i, factory.convertToString(eDataType, obj));
					}
					primitive.getValues().set(i,obj);
				}
			} else {
				Object obj = createObject(eDataType, primitive.getStringValues().get(i));
				if (obj != null){
					primitive.getValues().set(i,obj);
				}
			}
			
		}
		
	}

	private static final void createValue(Primitive primitive){
		if (primitive == null) return;
		
		EDataType eDataType = primitive.getType();
		if (eDataType == null) return;
		
		EPackage ePack = eDataType.getEPackage();
		String nsURI = ePack.getNsURI();
		EFactory factory = EPackage.Registry.INSTANCE.getEFactory(nsURI);
	
		if (primitive.getStringValue() == null){
			Object obj = eDataType.getDefaultValue();
			if (obj != null){
				if (factory != null){
					primitive.setStringValue(factory.convertToString(eDataType, obj));
				}
				primitive.setValue(obj);
			}
		} else {
			Object obj = createObject(eDataType, primitive.getStringValue());
			if (obj != null){
				primitive.setValue(obj);
			}
		}
		
	}

	private static void createDirectSingleObject(PrimitiveValue prim) {
		if (prim instanceof Primitive){
			createValue((Primitive) prim);
		}
		else if (prim instanceof MultiPrimitive){
			createValue((MultiPrimitive) prim);
		}
	}

	private static final void createDirectEObject(
			ReferenceValue refVal
			, EObject specEObject
			, DataStore dataStore
			, int numObjects){

		if (refVal instanceof Reference){
			((Reference) refVal).setReference(specEObject);
			if (!dataStore.getEObjects().contains(specEObject)){
				dataStore.getEObjects().add(specEObject);
			}
			EList<EObject> list = new BasicEList<EObject>();
			list.add(specEObject);
			dataStore.getKnownEObjects().put(refVal.getName(), list);
		}
		if (refVal instanceof MultiReference){
			EList<EObject> list = new BasicEList<EObject>();
			list.add(specEObject);
			if (numObjects > 1){
				for (int i = 1; i< numObjects ; i++){
					EObject eObj = createEObject(specEObject.eClass());
					dataStore.getEObjects().add(eObj);
					list.add(eObj);
				}
			}
			if (!dataStore.getEObjects().contains(specEObject)){
				dataStore.getEObjects().add(specEObject);
			}
			((MultiReference) refVal).getReferences().addAll(list);
			dataStore.getKnownEObjects().put(refVal.getName(), list);
		}
		
	}
	
	private static final EList<Value> buildReferencedValuesLis(
			ReferenceValue refVal, Action action){

		EList<Value> refValues = new BasicEList<Value>();
		if (refVal instanceof Reference){
			EObject refObj = ((Reference)refVal).getReference();
			if (refObj instanceof Value){
				refValues.add((Value)refObj);				
			} else {
				// Our action is not valid...
				ExecutionException execEx = new ExecutionException(
						"Invalid Action :" + action);
				throw execEx;					
			}	
		}
		if (refVal instanceof MultiReference){
			for (EObject refObj : ((MultiReference)refVal).getReferences()){
				if (refObj instanceof Value){
					refValues.add((Value)refObj);				
				} else {
					// Our action is not valid...
					ExecutionException execEx = new ExecutionException(
							"Invalid Action :" + action);
					throw execEx;					
				}	
			}
		}
		return refValues;
	}
}
